package com.talkyun.openx.ocean.codec;

import com.talkyun.ocean.model.Service;
import com.talkyun.openx.ocean.wrapper.RegistryableServiceFactory;
import com.talkyun.openx.server.codec.AbstractCodec;
import com.talkyun.openx.server.core.ServiceRequest;
import com.talkyun.utils.json.JSON;
import com.talkyun.utils.json.JSONObject;
import com.talkyun.utils.para.ParaNamer;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;

public class RegistryableJsonCodec extends AbstractCodec {
    private static final String HEAD_KEY = "_openx_head";
    private ParaNamer namer = new ParaNamer();
    private MethodFinder methodFinder = new MethodFinder();
    private RegistryableServiceFactory factory;
    private String context;

    public RegistryableJsonCodec(String context, RegistryableServiceFactory sf) {
        this.context = context;
        this.factory = sf;
    }

    public ServiceRequest decode(String uri, String data, String session) {
        ServiceRequest sr = this.decodeUri(uri);
        if (sr == null) {
            return null;
        }

        // if not fill data, return null
        try {
            return this.fillData(sr, session, data);
        } catch (ClassNotFoundException e) {
            return null;
        }
    }

    private ServiceRequest decodeUri(String uri) {
        // ex: uri => / {servlet ctx} / mapping / method
        int ms = uri.lastIndexOf("/");
        String method = uri.substring(ms + 1);
        String mapping = super.isBlank(context) ? uri.substring(0, ms) : uri.substring(context.length() - 1);

        if (super.isBlank(mapping) || super.isBlank(method)) {
            return null;
        }

        return new ServiceRequest(mapping, method);
    }

    private ServiceRequest fillData(ServiceRequest sr, String session, String data) throws ClassNotFoundException {
        String mapping = sr.getMapping().toLowerCase();
        String methodName = sr.getMethod().toLowerCase();

        Service service = factory.getServiceInfo(mapping);
        if (service == null) {
            return null;
        }

        sr.setSession(session);

        String className = service.getInterfaceName();
        JSONObject json = JSON.parseObject(super.isBlank(data) ? "{}" : data);
        // int methodArgNum = json.exists(HEAD_KEY) ? json.size() - 1 : json.size();
        // Method method = getMethod(Class.forName(className), methodName, methodArgNum);

        // Enhance more better method finder
        Method method = methodFinder.findBestMatchMethod(Class.forName(className), methodName, json);
        if (method == null) {
            return null;
        }

        // fill head
        JSONObject head = json.getJSONObject(HEAD_KEY);
        if (head != null) {
            for (Map.Entry<String, Object> entry : head.toMap().entrySet()) {
                Object val = entry.getValue();
                sr.head(entry.getKey(), val == null ? null : val.toString());
            }
        }

        // fill args
        List<ParaNamer.Param> list = namer.getParaList(method);
        if (list == null || list.isEmpty()) {
            return sr;
        }

        for (ParaNamer.Param param : list) {
            String argName = param.getName();
            Object argJson = json.get(argName);
            Type argType = param.getType();
            if (argJson == null) {
                sr.getArgs().put(argName, null);
            } else {
                sr.getArgs().put(argName, this.doDecode(argType, argJson));
            }
        }
        return sr;
    }

    private Object doDecode(Type type, Object json) {
        if (type instanceof Class<?>) {
            Class clazz = (Class) type;
            if (clazz.isEnum()) {
                String str = json == null ? null : json.toString().trim();
                // BUG handle null or "" decode to null (enum)
                if (str == null || str.length() == 0) {
                    return null;
                }

                // enum ordinal
                if (str.length() == 0 || Character.isDigit(str.charAt(0))) {
                    return JSON.toJavaObject(str, (Class<?>) type);
                }
                // enum name, fix input
                str = str.startsWith("\"") ? str : "\"" + str;
                str = str.endsWith("\"") ? str : str + "\"";
                return JSON.toJavaObject(str, (Class<?>) type);
            } else if (clazz == String.class) {
                return json.toString();
            } else if (clazz == Date.class) {
                if (json instanceof Number) {
                    return new Date(((Number) json).longValue());
                } else {
                    String date = json.toString().trim();
                    // "yyyy-MM-dd"
                    if (date.length() == "yyyy-MM-dd".length()) {
                        return parseDate(date, "yyyy-MM-dd");
                    } else {
                        // "yyyy-MM-dd HH:mm:ss"
                        return parseDate(date, "yyyy-MM-dd HH:mm:ss");
                    }
                }
            } else {
                return JSON.toJavaObject(json.toString(), (Class<?>) type);
            }
        }
        // generic type
        if (type instanceof ParameterizedType) {
            ParameterizedType pt = ((ParameterizedType) type);
            Class<?> clazz = (Class<?>) pt.getRawType();
            // Generic List<E>
            if (clazz == List.class) {
                Class<?> targetClazz = (Class<?>) pt.getActualTypeArguments()[0];
                return JSON.toJavaList(json.toString(), targetClazz);
            }
            throw new RuntimeException("Not support type " + type);
        } else {
            throw new RuntimeException("Not support type " + type);
        }
    }

    private Date parseDate(String src, String fmt) {
        try {
            return new SimpleDateFormat(fmt).parse(src);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }
}